/**
 * @module vim-loader/materials
 */

import {
  Material,
  LineBasicMaterial,
  ShaderMaterial,
  Plane,
  Color,
  EventDispatcher,
  BaseEvent,
} from 'three'
import { StandardMaterial, createOpaque, createTransparent } from './standardMaterial'
import { createMaskMaterial } from './maskMaterial'
import { createIsolationMaterial } from './isolationMaterial'
import { OutlineMaterial } from './outlineMaterial'
import { MergeMaterial } from './mergeMaterial'
import { createSimpleMaterial } from './simpleMaterial'

export interface MaterialSetting {
  /**
  * Highlight on hover options
  */
  highlight: {
    /**
    * Highlight color
    * Default: rgb(106, 210, 255)
    */
    color: Color
    /**
    * Highlight opacity
    * Default: 0.5
    */
    opacity: number
  }
  /**
  * Isolation material options
  */
  isolation: {
    /**
    * Isolation material color
    * Default: rgb(78, 82, 92)
    */
    color: Color
    /**
    * Isolation material opacity
    * Default: 0.08
    */
    opacity: number
  }
  /**
  * Section box intersection highlight options
  */
  section: {
    /**
    * Intersection highlight stroke width.
    * Default: 0.01
    */
    strokeWidth: number;
    /**
    * Intersection highlight stroke falloff.
    * Default: 0.75
    */
    strokeFalloff: number;
    /**
    * Intersection highlight stroke color.
    * Default: rgb(246, 246, 246)
    */
    strokeColor: Color;
  }
  /**
  * Selection outline options
  */
  outline: {
    /**
    * Selection outline intensity.
    * Default: 3
    */
    intensity: number;
    /**
    * Selection outline falloff.
    * Default: 3
    */
    falloff: number;
    /**
    * Selection outline blur.
    * Default: 2
    */
    blur: number;
    /**
    * Selection outline color.
    * Default: rgb(0, 255, 255)
    */
    color: Color;
  }
}
export class MaterialUpdateEvent implements BaseEvent<EventType> {
  type: EventType = 'update'
}
type EventType = keyof MaterialEventMap
interface MaterialEventMap {
  update: MaterialUpdateEvent
}
/**
 * Defines the materials to be used by the vim loader and allows for material injection.
 */
export class MaterialsTool extends EventDispatcher<MaterialEventMap> {
  static instance: MaterialsTool

  static createInstance(instance: MaterialsTool) {
    this.instance = instance
  }

  static getInstance() {
    if (!this.instance) {
      this.instance = new MaterialsTool()
    }
    return this.instance
  }

  /**
   * Material used for opaque model geometry.
   */
  opaque: StandardMaterial
  /**
   * Material used for transparent model geometry.
   */
  transparent: StandardMaterial
  /**
   * Material used for maximum performance.
   */
  simple: Material
  /**
   * Material used when creating wireframe geometry of the model.
   */
  wireframe: LineBasicMaterial
  /**
   * Material used to show traces of hidden objects.
   */
  isolation: Material
  /**
   * Material used to filter out what is not selected for selection outline effect.
   */
  mask: ShaderMaterial
  /**
   * Material used for selection outline effect.
   */
  outline: OutlineMaterial

  /**
   * Material used to merge outline effect with scene render.
   */
  merge: MergeMaterial

  private _clippingPlanes: Plane[] | undefined
  private _sectionStrokeWitdh: number = 0.01
  private _sectionStrokeFallof: number = 0.75
  private _sectionStrokeColor: Color = new Color(0xf6, 0xf6, 0xf6)
  private _focusIntensity: number = 0.75
  private _focusColor: Color = new Color(1, 1, 1)



  constructor(
    opaque?: StandardMaterial,
    transparent?: StandardMaterial,
    simple?: Material,
    wireframe?: LineBasicMaterial,
    isolation?: Material,
    mask?: ShaderMaterial,
    outline?: OutlineMaterial,
    merge?: MergeMaterial
  ) {
    super()
    this.opaque = opaque ?? createOpaque()
    this.transparent = transparent ?? createTransparent()
    this.simple = simple ?? createSimpleMaterial()
    this.wireframe = wireframe ?? createWireframe()
    this.isolation = isolation ?? createIsolationMaterial()
    this.mask = mask ?? createMaskMaterial()
    this.outline = outline ?? new OutlineMaterial()
    this.merge = merge ?? new MergeMaterial()
  }

  /**
   * Updates material settings based on the provided configuration.
   * @param {ViewerSettings} settings - The settings to apply to the materials.
   */
  applySettings(settings: MaterialSetting) {
    this.isolationOpacity = settings.isolation.opacity
    this.isolationColor = settings.isolation.color

    this.wireframeColor = settings.highlight.color
    this.wireframeOpacity = settings.highlight.opacity

    this.sectionStrokeWitdh = settings.section.strokeWidth
    this.sectionStrokeFallof = settings.section.strokeFalloff
    this.sectionStrokeColor = settings.section.strokeColor

    this.outlineIntensity = settings.outline.intensity
    this.outlineFalloff = settings.outline.falloff
    this.outlineBlur = settings.outline.blur
    this.outlineColor = settings.outline.color
    return this
  }

  /**
   * A signal dispatched whenever a material is modified.
   */
  get onUpdate() {
    return this.dispatchEvent(new MaterialUpdateEvent())
  }

  /**
   * Determines the opacity of the isolation material.
   */
  get isolationOpacity() {
    return this.isolation.opacity
  }

  set isolationOpacity(opacity: number) {
    const mat = this.isolation as ShaderMaterial
    mat.uniforms.opacity.value = opacity
    mat.uniformsNeedUpdate = true
    this.dispatchEvent(new MaterialUpdateEvent())
  }

  /**
   * Determines the color of the isolation material.
   */
  get isolationColor(): Color {
    const mat = this.isolation as ShaderMaterial
    return mat.uniforms.fillColor.value
  }

  set isolationColor(color: Color) {
    const mat = this.isolation as ShaderMaterial
    mat.uniforms.fillColor.value = color
    mat.uniformsNeedUpdate = true
    this.dispatchEvent(new MaterialUpdateEvent())
  }

  /**
  * Determines the color intensity of the highlight effect on mouse hover.
  */
  get focusIntensity() {
    return this._focusIntensity
  }

  set focusIntensity(value: number) {
    if (this._focusIntensity === value) return
    this._focusIntensity = value
    this.opaque.focusIntensity = value
    this.transparent.focusIntensity = value
    this.dispatchEvent(new MaterialUpdateEvent())
  }

  /**
   * Determines the color of the highlight effect on mouse hover.
   */
  get focusColor() {
    return this._focusColor
  }

  set focusColor(value: Color) {
    if (this._focusColor === value) return
    this._focusColor = value
    this.opaque.focusColor = value
    this.transparent.focusColor = value
    this.dispatchEvent(new MaterialUpdateEvent())
  }

  /**
   * Determines the color of wireframe meshes.
   */
  get wireframeColor() {
    return this.wireframe.color
  }

  set wireframeColor(value: Color) {
    if (this.wireframe.color === value) return
    this.wireframe.color = value
    this.dispatchEvent(new MaterialUpdateEvent())
  }

  /**
   * Determines the opacity of wireframe meshes.
   */
  get wireframeOpacity() {
    return this.wireframe.opacity
  }

  set wireframeOpacity(value: number) {
    if (this.wireframe.opacity === value) return

    this.wireframe.opacity = value
    this.dispatchEvent(new MaterialUpdateEvent())
  }

  /**
   * The clipping planes applied to all relevent materials
   */
  get clippingPlanes() {
    return this._clippingPlanes
  }

  set clippingPlanes(value: Plane[] | undefined) {
    // THREE Materials will break if assigned undefined
    this._clippingPlanes = value
    this.opaque.clippingPlanes = value ?? null
    this.transparent.clippingPlanes = value ?? null
    this.wireframe.clippingPlanes = value ?? null
    this.isolation.clippingPlanes = value ?? null
    this.mask.clippingPlanes = value ?? null
    this.dispatchEvent(new MaterialUpdateEvent())
  }

  /**
   * The width of the stroke effect where the section box intersects the model.
   */
  get sectionStrokeWitdh() {
    return this._sectionStrokeWitdh
  }

  set sectionStrokeWitdh(value: number) {
    if (this._sectionStrokeWitdh === value) return
    this._sectionStrokeWitdh = value
    this.opaque.sectionStrokeWitdh = value
    this.transparent.sectionStrokeWitdh = value
    this.dispatchEvent(new MaterialUpdateEvent())
  }

  /**
   * Gradient of the stroke effect where the section box intersects the model.
   */
  get sectionStrokeFallof() {
    return this._sectionStrokeFallof
  }

  set sectionStrokeFallof(value: number) {
    if (this._sectionStrokeFallof === value) return
    this._sectionStrokeFallof = value
    this.opaque.sectionStrokeFallof = value
    this.transparent.sectionStrokeFallof = value
    this.dispatchEvent(new MaterialUpdateEvent())
  }

  /**
   * Color of the stroke effect where the section box intersects the model.
   */
  get sectionStrokeColor() {
    return this._sectionStrokeColor
  }

  set sectionStrokeColor(value: Color) {
    if (this._sectionStrokeColor === value) return
    this._sectionStrokeColor = value
    this.opaque.sectionStrokeColor = value
    this.transparent.sectionStrokeColor = value
    this.dispatchEvent(new MaterialUpdateEvent())
  }

  /**
   * Color of the selection outline effect.
   */
  get outlineColor() {
    return this.merge.color
  }

  set outlineColor(value: Color) {
    if (this.merge.color === value) return
    this.merge.color = value
    this.dispatchEvent(new MaterialUpdateEvent())
  }

  /**
   * Size of the blur convolution on the selection outline effect.
   */
  get outlineBlur() {
    return this.outline.strokeBlur
  }

  set outlineBlur(value: number) {
    if (this.outline.strokeBlur === value) return
    this.outline.strokeBlur = value
    this.dispatchEvent(new MaterialUpdateEvent())
  }

  /**
   * Gradient of the the selection outline effect.
   */
  get outlineFalloff() {
    return this.outline.strokeBias
  }

  set outlineFalloff(value: number) {
    if (this.outline.strokeBias === value) return
    this.outline.strokeBias = value
    this.dispatchEvent(new MaterialUpdateEvent())
  }

  /**
   * Intensity of the the selection outline effect.
   */
  get outlineIntensity() {
    return this.outline.strokeMultiplier
  }

  set outlineIntensity(value: number) {
    if (this.outline.strokeMultiplier === value) return
    this.outline.strokeMultiplier = value
    this.dispatchEvent(new MaterialUpdateEvent())
  }

  /** dispose all materials. */
  dispose() {
    this.opaque.dispose()
    this.transparent.dispose()
    this.wireframe.dispose()
    this.isolation.dispose()
    this.mask.dispose()
    this.outline.dispose()
  }
}

/**
 * Creates a new instance of the default wireframe material.
 * @returns {LineBasicMaterial} A new instance of LineBasicMaterial.
 */
export function createWireframe() {
  const material = new LineBasicMaterial({
    depthTest: false,
    opacity: 1,
    color: new Color(0x0000ff),
    transparent: true
  })
  return material
}
